package qiniu

import (
	"bytes"
	"context"
	"fmt"
	"mime/multipart"
	"os"
	"path/filepath"
	"strings"
	"time"

	"github.com/qiniu/go-sdk/v7/auth"
	"github.com/qiniu/go-sdk/v7/storage"
)

/*
 * 参考资料: https://developer.qiniu.com/kodo/1238/go
 */

// 七牛云上传返回结构体
type ReturnRet struct {
	Key    string `json:"key"`
	Hash   string `json:"hash"`
	Fsize  int    `json:"fsize"`
	Bucket string `json:"bucket"`
	Name   string `json:"name"`
	Url    string `json:"url"`
}

// 自定义的 bucket 配置结构
type BucketCfg struct {
	BaseUri   string // 访问域名
	Bucket    string // 上传空间名
	IsPrivate bool   // 上传空间名
	IsDefault bool   // 是否默认 bucket
	Expires   uint64 // 上传凭证有效期， 单位 秒
	Token     string // 生成token赋值在结构上
}

/*
 * 封装的七牛云上传客户端
 */
type QiNiu struct {
	Cfg *storage.Config        // 七牛云上传配置
	Mac *auth.Credentials      // 客户端上传凭证
	Bm  *storage.BucketManager // 空间管理组件

	BucketCfgs    map[string]*BucketCfg // bucket 配置
	DefaultBucket string                // 默认 bucket
}

/**
 * 七牛云上传组件配置数据结构
 */
type Conf struct {
	AccessKey string
	SecretKey string
}

/*
 * 初始化 QiNiu 并返回 QiNiu 实例
 *
 * @param accessKey 七牛云签名公钥
 * @param secretKey 七牛云签名密钥
 *
 * @return *QiNiu 七牛云上传客户端
 */
func New(conf *Conf) *QiNiu {
	cfg := &storage.Config{}
	mac := auth.New(conf.AccessKey, conf.SecretKey)
	bm := storage.NewBucketManager(mac, cfg)
	qn := &QiNiu{
		Cfg:        cfg,
		Mac:        mac,
		Bm:         bm,
		BucketCfgs: make(map[string]*BucketCfg),
	}

	return qn
}

/*
 * 根据上传配置获取 token
 */
func (qn *QiNiu) GenerateToken(cfg *BucketCfg) string {
	putPolicy := storage.PutPolicy{
		Scope:      cfg.Bucket,
		ReturnBody: `{"key":"$(key)","hash":"$(etag)","fsize":$(fsize),"bucket":"$(bucket)","name":"$(x:name)"}`,
	}
	if cfg.Expires > 0 {
		putPolicy.Expires = cfg.Expires
	}
	return putPolicy.UploadToken(qn.Mac)
}

/*
 * 增加 bucket 配置
 */
func (qn *QiNiu) AddBucket(cfg *BucketCfg) *QiNiu {
	qn.BucketCfgs[cfg.Bucket] = cfg
	if cfg.IsDefault {
		qn.DefaultBucket = cfg.Bucket
	}

	// 定时更新 token
	go func() {
		ticker := time.NewTicker(time.Second * time.Duration(cfg.Expires-60))
		defer ticker.Stop()

		cfg.Token = qn.GenerateToken(cfg)
		// 定时更新 token
		for {
			select {
			case <-ticker.C:
				cfg.Token = qn.GenerateToken(cfg)
			}
		}
	}()

	return qn
}

/*
 * 获取操作 bucket 配置
 */
func (qn *QiNiu) GetBucketCfg(bucket ...any) (*BucketCfg, error) {
	if len(bucket) == 0 {
		if cfg, ok := qn.BucketCfgs[qn.DefaultBucket]; ok {
			return cfg, nil
		}
		return nil, fmt.Errorf("Bucket name is required")
	}

	switch bucket[0].(type) {
	case string:
		bt := bucket[0].(string)
		if bt == "" {
			return qn.GetBucketCfg()
		}
		if cfg, ok := qn.BucketCfgs[bt]; ok {
			return cfg, nil
		}
		return nil, fmt.Errorf("Bucket must be appended before")
	case BucketCfg:
		bt := bucket[0].(BucketCfg)
		if bt.Token == "" {
			bt.Token = qn.GenerateToken(&bt)
		}
		return &bt, nil
	case *BucketCfg:
		bt := bucket[0].(*BucketCfg)
		if bt.Token == "" {
			bt.Token = qn.GenerateToken(bt)
		}
		return bt, nil
	default:
		return nil, fmt.Errorf("Unknown bucket type: %v", bucket[0])
	}
}

/*
 * 获取自定义上传凭证
 * storage.PutPolicy{
 *     Scope:            bucket,
 *     CallbackURL:      "http://api.example.com/qiniu/upload/callback", // 上传成功后自动回调
 *     CallbackBody:     `{"key":"$(key)","hash":"$(etag)","fsize":$(fsize),"bucket":"$(bucket)","name":"$(x:name)"}`,
 *     CallbackBodyType: "application/json", // 回调类型
 * }
 */
func (qn *QiNiu) GenerateCustomToken(putPolicy storage.PutPolicy, bucket ...any) (string, error) {
	cfg, err := qn.GetBucketCfg(bucket...)
	if err != nil {
		return "", err
	}

	if putPolicy.Scope == "" {
		putPolicy.Scope = cfg.Bucket
	}
	if putPolicy.ReturnBody == "" {
		putPolicy.ReturnBody = `{"key":"$(key)","hash":"$(etag)","fsize":$(fsize),"bucket":"$(bucket)","name":"$(x:name)"}`
	}
	if cfg.Expires > 0 {
		putPolicy.Expires = cfg.Expires
	}
	return putPolicy.UploadToken(qn.Mac), nil
}

/*
 * 获取简单上传凭证
 */
func (qn *QiNiu) GenerateUploadToken(bucket ...any) (string, error) {
	cfg, err := qn.GetBucketCfg(bucket...)
	if err != nil {
		return "", err
	}

	putPolicy := storage.PutPolicy{
		Scope:      cfg.Bucket,
		ReturnBody: `{"key":"$(key)","hash":"$(etag)","fsize":$(fsize),"bucket":"$(bucket)","name":"$(x:name)"}`,
	}
	if cfg.Expires > 0 {
		putPolicy.Expires = cfg.Expires
	}
	return qn.GenerateCustomToken(putPolicy)
}

/*
 * 获取覆盖上传凭证
 */
func (qn *QiNiu) GenerateCoverToken(key string, bucket ...any) (string, error) {
	cfg, err := qn.GetBucketCfg(bucket...)
	if err != nil {
		return "", err
	}
	pp := storage.PutPolicy{
		Scope:      fmt.Sprintf("%s:%s", cfg.Bucket, key),
		ReturnBody: `{"key":"$(key)","hash":"$(etag)","fsize":$(fsize),"bucket":"$(bucket)","name":"$(x:name)"}`,
	}
	return qn.GenerateCustomToken(pp)
}

/*
 * 获取保存文件名称
 */
func (qn *QiNiu) GenerateKey(file string, prefix ...string) string {
	ext := filepath.Ext(file)
	key := ""
	if len(prefix) == 0 || prefix[0] == "" {
		key += fmt.Sprintf("%s/", "default")
	} else {
		key += fmt.Sprintf("%s/", strings.TrimRight(prefix[0], "/"))
	}
	if ext == "" {
		return fmt.Sprintf("%s%s%06d", key, time.Now().Format("20060102030405"), time.Now().UnixMilli()%100000)
	}
	return fmt.Sprintf("%s%s%06d%s", key, time.Now().Format("20060102030405"), time.Now().UnixMilli()%100000, ext)
}

/*
 * 通过 token 上传 form 文件
 * key: 保存文件名
 * token: 可选的上传凭证
 */
func (qn *QiNiu) UploadFormByToken(fileHeader *multipart.FileHeader, key string, token string) (ret ReturnRet, err error) {
	fp, err := fileHeader.Open()
	if err != nil {
		return
	}
	defer fp.Close()
	uploader := storage.NewFormUploader(qn.Cfg)
	err = uploader.Put(context.Background(), &ret, token, key, fp, fileHeader.Size, &storage.PutExtra{})
	return
}

/*
 * 通过 bucket 上传 form 文件
 * key: 保存文件名
 * token: 可选的上传凭证
 */
func (qn *QiNiu) UploadFormByBucket(fileHeader *multipart.FileHeader, key string, bucket ...any) (ret ReturnRet, err error) {
	var cfg *BucketCfg
	if cfg, err = qn.GetBucketCfg(bucket...); err != nil {
		return
	}
	return qn.UploadFormByToken(fileHeader, key, cfg.Token)
}

/*
 * 通过 token 上传字符串文本数据
 * prefix: 保存文件前缀
 * token: 可选的上传凭证
 */
func (qn *QiNiu) UploadStringByToken(data string, prefix string, token string) (ret ReturnRet, err error) {
	uploader := storage.NewFormUploader(qn.Cfg)
	bs := []byte(data)
	key := qn.GenerateKey("...txt", prefix)

	err = uploader.Put(context.Background(), &ret, token, key, bytes.NewReader(bs), int64(len(bs)), &storage.PutExtra{})
	return
}

/*
 * 通过 bucket 上传字符串文本数据
 * prefix: 保存文件前缀
 * token: 可选的上传凭证
 */
func (qn *QiNiu) UploadStringByBucket(data string, prefix string, bucket ...any) (ret ReturnRet, err error) {
	var cfg *BucketCfg
	if cfg, err = qn.GetBucketCfg(bucket...); err != nil {
		return
	}
	return qn.UploadStringByToken(data, prefix, cfg.Token)
}

// 上传 url
func (qn *QiNiu) UploadUrlByBucket(url string, prefix string, bucket ...any) (ret ReturnRet, err error) {
	var cfg *BucketCfg
	if cfg, err = qn.GetBucketCfg(bucket...); err != nil {
		return
	}
	key := qn.GenerateKey(url, prefix)

	retFech, err := qn.Bm.Fetch(url, cfg.Bucket, key)
	if err != nil {
		return
	}
	ret.Key = retFech.Key
	ret.Hash = retFech.Hash
	ret.Fsize = int(retFech.Fsize)
	ret.Bucket = cfg.Bucket
	ret.Name = key
	return
}

/*
 * 通过 token 上传本地文件
 * prefix: 保存文件前缀
 * token: 可选的上传凭证
 */
func (qn *QiNiu) UploadFileByToken(localFile string, prefix string, token string) (ret ReturnRet, err error) {
	uploader := storage.NewFormUploader(qn.Cfg)
	key := qn.GenerateKey(localFile, prefix)
	err = uploader.PutFile(context.Background(), &ret, token, key, localFile, &storage.PutExtra{})
	return
}

/*
 * 通过 bucket 上传本地文件
 * prefix: 保存文件前缀
 * token: 可选的上传凭证
 */
func (qn *QiNiu) UploadFileByBucket(localFile string, prefix string, bucket ...any) (ret ReturnRet, err error) {
	var cfg *BucketCfg
	if cfg, err = qn.GetBucketCfg(bucket...); err != nil {
		return
	}
	return qn.UploadFileByToken(localFile, prefix, cfg.Token)
}

/*
 * 通过 token 上传本地文件: 分片上传(推荐)
 * prefix: 保存文件前缀
 * token: 可选的上传凭证
 */
func (qn *QiNiu) PartUploadFileByToken(localFile string, key string, token string) (ret ReturnRet, err error) {
	uploader := storage.NewResumeUploaderV2(qn.Cfg)
	putExtra := &storage.RputV2Extra{
		PartSize: 1024 * 1024,
	}
	err = uploader.PutFile(context.Background(), &ret, token, key, localFile, putExtra)
	return
}

/*
 * 通过 bucket 上传本地文件: 分片上传(推荐)
 * prefix: 保存文件前缀
 * token: 可选的上传凭证
 */
func (qn *QiNiu) PartUploadFileByBucket(localFile string, key string, bucket ...any) (ret ReturnRet, err error) {
	var cfg *BucketCfg
	if cfg, err = qn.GetBucketCfg(bucket...); err != nil {
		return
	}
	return qn.PartUploadFileByToken(localFile, key, cfg.Token)
}

/*
 * 通过 token 上传本地文件: 断点上传
 * prefix: 保存文件前缀
 * token: 可选的上传凭证
 */
func (qn *QiNiu) PointUploadFileByToken(localFile string, key string, token string) (ret ReturnRet, err error) {
	uploader := storage.NewResumeUploaderV2(qn.Cfg)
	recorder, err := storage.NewFileRecorder(os.TempDir())
	if err != nil {
		return
	}
	putExtra := storage.RputV2Extra{
		Recorder: recorder,
	}
	err = uploader.PutFile(context.Background(), &ret, token, key, localFile, &putExtra)
	return
}

/*
 * 通过 bucket 上传本地文件: 断点上传
 * prefix: 保存文件前缀
 * token: 可选的上传凭证
 */
func (qn *QiNiu) PointUploadFileByBucket(localFile string, key string, bucket ...any) (ret ReturnRet, err error) {
	var cfg *BucketCfg
	if cfg, err = qn.GetBucketCfg(bucket...); err != nil {
		return
	}
	return qn.PointUploadFileByToken(localFile, key, cfg.Token)
}

/*
 * 获取访问路径: 公有空间
 */
func (qn *QiNiu) PublicAccessUrl(key string, baseUri string) string {
	return storage.MakePublicURL(baseUri, key)
}

/*
 * 获取访问路径: 公有空间
 */
func (qn *QiNiu) PrivateAccessUrl(key string, baseUri string, expire ...time.Duration) string {
	var deadline int64
	if len(expire) > 0 && expire[0] > 0 {
		deadline = time.Now().Add(time.Second * expire[0]).Unix()
	} else {
		deadline = time.Now().Add(time.Second * 3600).Unix() //1小时有效期
	}
	return storage.MakePrivateURL(qn.Mac, baseUri, key, deadline)
}

/*
 * 通过 bucket 获取访问路径
 */
func (qn *QiNiu) AccessUrlByBucket(key string, bucket ...any) string {
	cfg, err := qn.GetBucketCfg(bucket...)
	if err != nil {
		return "unknown"
	}
	if cfg.IsPrivate {
		return qn.PrivateAccessUrl(key, cfg.BaseUri)
	}
	return qn.PublicAccessUrl(key, cfg.BaseUri)
}

/*
 * 获取文件信息
 */
func (qn *QiNiu) Stat(key string, bucket ...any) (fInfo storage.FileInfo, err error) {
	cfg, err := qn.GetBucketCfg(bucket...)
	if err != nil {
		return
	}
	return qn.Bm.Stat(cfg.Bucket, key)
}

/*
 * 修改文件 MimeType
 */
func (qn *QiNiu) ChangeMime(key string, mime string, bucket ...any) (err error) {
	cfg, err := qn.GetBucketCfg(bucket...)
	if err != nil {
		return
	}
	return qn.Bm.ChangeMime(cfg.Bucket, key, mime)
}

/*
 * 修改文件存储类型
 *   0: 表示普通存储
 *   1: 表示低频存储
 *   2: 表示归档存储
 *   3: 表示深度归档存储
 *   4: 表示归档直读存储
 */
func (qn *QiNiu) ChangeType(key string, fileType int, bucket ...any) error {
	cfg, err := qn.GetBucketCfg(bucket...)
	if err != nil {
		return err
	}
	return qn.Bm.ChangeType(cfg.Bucket, key, fileType)
}

/*
 * 文件移动， 可在同一个bucket内移动
 */
func (qn *QiNiu) Move(srcBucket, srcKey string, destBucket, destKey string, force bool) (err error) {
	return qn.Bm.Move(srcBucket, srcKey, destBucket, destKey, force)
}

/*
 * 文件复制， 可在同一个bucket内复制
 */
func (qn *QiNiu) Copy(srcBucket, srcKey string, destBucket, destKey string, force bool) (err error) {
	return qn.Bm.Copy(srcBucket, srcKey, destBucket, destKey, force)
}

/*
 * 删除文件
 */
func (qn *QiNiu) Delete(key string, bucket ...any) error {
	cfg, err := qn.GetBucketCfg(bucket...)
	if err != nil {
		return err
	}
	return qn.Bm.Delete(cfg.Bucket, key)
}

/*
 * 设置或更新生存时间，单位: 天
 */
func (qn *QiNiu) UpdateExpireDays(key string, days int, bucket ...any) error {
	cfg, err := qn.GetBucketCfg(bucket...)
	if err != nil {
		return err
	}
	return qn.Bm.DeleteAfterDays(cfg.Bucket, key, days)
}

/*
 * 获取指定前缀的文件列表
 */
func (qn *QiNiu) FileList(prefix string, limit int, bucket ...any) (keys []string, err error) {
	cfg, err := qn.GetBucketCfg(bucket...)
	if err != nil {
		return
	}
	marker := ""
	for {
		entries, _, nextMarker, hasNext, err := qn.Bm.ListFiles(cfg.Bucket, prefix, "", marker, limit)
		if err != nil {
			break
		}
		for _, entry := range entries {
			keys = append(keys, entry.Key)
		}
		if hasNext {
			marker = nextMarker
		} else {
			break
		}
	}
	return
}

/*
 * 批量获取文件信息
 * rets[i].Code == 200: 成功
 * rets[i].Data.Error: 错误信息
 */
func (qn *QiNiu) BatchStat(keys []string, bucket string) (rets []storage.BatchOpRet, err error) {
	statOps := make([]string, 0, len(keys))
	for _, key := range keys {
		statOps = append(statOps, storage.URIStat(bucket, key))
	}
	return qn.Bm.Batch(statOps)
}

/*
 * 批量删除文件
 */
func (qn *QiNiu) BatchDelete(keys []string, bucket ...any) (rets []storage.BatchOpRet, err error) {
	cfg, err := qn.GetBucketCfg(bucket...)
	if err != nil {
		return
	}
	delOptions := make([]string, 0, len(keys))
	for _, key := range keys {
		delOptions = append(delOptions, storage.URIDelete(cfg.Bucket, key))
	}
	return qn.Bm.Batch(delOptions)
}

/*
 * 批量更新文件生存时间
 */
func (qn *QiNiu) BatchUpdateExpireDays(keys []string, days int, bucket ...any) (rets []storage.BatchOpRet, err error) {
	cfg, err := qn.GetBucketCfg(bucket...)
	if err != nil {
		return
	}
	expireOptions := make([]string, 0, len(keys))
	for _, key := range keys {
		expireOptions = append(expireOptions, storage.URIDeleteAfterDays(cfg.Bucket, key, days))
	}
	return qn.Bm.Batch(expireOptions)
}

/*
 * 批量更新文件类型
 */
func (qn *QiNiu) BatchChangeType(keys []string, fileType int, bucket ...any) (rets []storage.BatchOpRet, err error) {
	cfg, err := qn.GetBucketCfg(bucket...)
	if err != nil {
		return
	}
	changeTypeOptions := make([]string, 0, len(keys))
	for _, key := range keys {
		changeTypeOptions = append(changeTypeOptions, storage.URIChangeType(cfg.Bucket, key, fileType))
	}
	return qn.Bm.Batch(changeTypeOptions)
}
